# Copyright (c) 2019 Presto Labs Pte. Ltd.
# Author: xguo

import base64
import enum
import hashlib
import hmac
import json
import logging
import time
import typing
import urllib.parse

import requests
import requests.auth

from coin.exchange.base.order_gateway import OrderSide
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.base.kr_rest.auth_base import AuthKey, AuthKeyContainer
from coin.exchange.util.rate_limit import RateLimiter
from coin.exchange.util.order_checker import OrderAmountChecker
from coin.exchange.base.rest.request_nonce_manager import RequestNonceManager
from coin.exchange.util.async_http_client import AsyncHttpClient


def is_success_response_msg(msg):
  error_code = int(msg['errorCode'])
  result = msg['result']
  return error_code == 0 and result == 'success'


class CoinoneOrderSide(enum.Enum):
  BUY = 'bid'
  SELL = 'ask'


COINONE_API_HOST = 'api.coinone.co.kr'


class CoinoneAuth(requests.auth.AuthBase):
  def __init__(self, key_file, rest_request_gap_sec=None, use_extra_keys=True):
    if use_extra_keys:
      self._auth_key_container = AuthKeyContainer.from_primary_key(key_file)
    else:
      self._auth_key_container = AuthKeyContainer.from_single_key(key_file)

    self._token = None
    self._auth_value = None
    self._nonce_manager = RequestNonceManager(request_gap_sec=rest_request_gap_sec)

  @property
  def num_of_keys(self):
    return len(self._auth_key_container)

  def __call__(self, req: requests.PreparedRequest):
    if req.method == 'GET':
      return req

    assert req.method == 'POST', req.method
    if req.body:
      params = json.loads(req.body)
    else:
      params = {}
    curr_key = self._auth_key_container.get_next_key_pair()
    params['nonce'] = self._nonce_manager.get_nonce()
    params['access_token'] = curr_key.access_key
    encoded_payload = base64.b64encode(json.dumps(params).encode())
    signature = hmac.new(curr_key.secret_key_upper_bytes, encoded_payload, hashlib.sha512)
    headers = {
        'X-COINONE-PAYLOAD': encoded_payload,
        'X-COINONE-SIGNATURE': signature.hexdigest(),
    }
    req.headers.update(headers)
    req.prepare_body(data=None, files=None, json=params)
    return req


class CoinoneSyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      rest_request_gap_sec=None,
      timeout=10,
      use_extra_keys=True,
  ):
    self._timeout = timeout
    self._auth = CoinoneAuth(key_file, rest_request_gap_sec, use_extra_keys)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or COINONE_API_HOST)
    self._logger = logging.getLogger(__name__)
    self.rate_limit = RateLimiter(10, 1.0)

  @property
  def num_of_keys(self):
    return self._auth.num_of_keys

  def auth_request(self, method, path, params):
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      response = requests.request(method, url, json=params, auth=self._auth, timeout=self._timeout)
      content = response.json()
      return content
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException: %s')
      raise
    except json.JSONDecodeError:
      self._logger.exception('Json decode: %s', response.content)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def _query_nocheck(self, method, path, params):
    req_timestamp = time.time()
    content = self.auth_request(method, path, params)
    res_timestamp = time.time()
    status = requests.codes.ok
    update = RestUpdate(req_timestamp, res_timestamp, content, status)
    return update

  def query(self, method, path, params=None, disable_rate_limit=False):
    if not disable_rate_limit:
      while not self.rate_limit.check():
        time.sleep(0.1)
    else:
      self.rate_limit.check()
    return self._query_nocheck(method, path, params)


class CoinoneAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=4,
      rest_request_gap_sec=None,
      use_extra_keys=True,
  ):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = CoinoneAuth(key_file, rest_request_gap_sec, use_extra_keys)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or COINONE_API_HOST)

  @property
  def num_of_keys(self):
    return self._auth.num_of_keys

  def query(self, method, path, params=None):
    url = urllib.parse.urljoin(self._api_host, path)
    future = self._http_client.fetch(
        method=method,
        url=url,
        json=params,
        auth=self._auth,
        timeout=self._timeout,
    )
    return future


class CoinoneNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      use_async=False,
      rest_request_gap_sec=None,
      use_extra_keys=True,
  ):
    api_host = api_host or COINONE_API_HOST
    if use_async:
      self._auth_queryer = CoinoneAsyncQueryer(
          key_file,
          api_host=api_host,
          disable_ssl=disable_ssl,
          timeout=timeout,
          max_connections=max_connections,
          rest_request_gap_sec=rest_request_gap_sec,
          use_extra_keys=use_extra_keys,
      )
    else:
      self._auth_queryer = CoinoneSyncQueryer(
          key_file,
          api_host=api_host,
          timeout=timeout,
          rest_request_gap_sec=rest_request_gap_sec,
          use_extra_keys=use_extra_keys,
      )
    self.order_amount_checker = OrderAmountChecker("KRW", 500, 50000000)

  def _query(self, method, path, params=None):
    return self._auth_queryer.query(method, path, params=params)

  @property
  def num_of_keys(self):
    return self._auth_queryer.num_of_keys

  def query_account_balance(self):
    method = 'POST'
    path = 'v2/account/balance'
    update = self._query(method, path)
    return update

  def query_account_user_info(self):
    method = 'POST'
    path = 'v2/account/user_info'
    update = self._query(method, path)
    return update

  def cancel_order(
      self,
      *,
      order_id: str,
      currency: typing.Optional[str] = None,
      price=None,
      qty=None,
      order_side: typing.Optional[OrderSide] = None,
  ):
    method = 'POST'
    path = 'v2/order/cancel'
    is_ask = int(order_side == OrderSide.SELL)  # 1 or 0
    params = {
        'order_id': order_id,
        'price': price,
        'qty': qty,
        'is_ask': int(is_ask),
        'currency': currency,
    }
    update = self._query(method, path, params)
    return update

  def place_buy_limit_order(self, *, currency, price, qty):
    method = 'POST'
    path = 'v2/order/limit_buy'
    params = {
        'currency': currency,
        'price': price,
        'qty': qty,
    }
    self.order_amount_checker.check_amount('KRW', float(price), float(qty))
    update = self._query(method, path, params)
    return update

  def place_sell_limit_order(self, currency, price, qty):
    method = 'POST'
    path = 'v2/order/limit_sell'
    params = {
        'currency': currency,
        'price': price,
        'qty': qty,
    }
    self.order_amount_checker.check_amount('KRW', float(price), float(qty))
    update = self._query(method, path, params)
    return update

  def query_order_info(self, order_id, currency):
    method = 'POST'
    path = 'v2/order/order_info'
    params = {'order_id': order_id, 'currency': currency}
    update = self._query(method, path, params)
    return update

  def query_orders(self, currency):
    method = 'POST'
    path = 'v2/order/limit_orders'
    params = {'currency': currency}
    update = self._query(method, path, params)
    return update

  def query_complete_orders(self, currency):
    method = 'POST'
    path = 'v2/order/complete_orders'
    params = {'currency': currency}
    update = self._query(method, path, params)
    return update

  def query_orderbook(self, currency):
    method = 'GET'
    path = 'orderbook'
    params = {'currency': currency}
    update = self._query(method, path, params)
    return update

  def query_trades(self, currency):
    method = 'GET'
    path = 'trades'
    params = {'currency': currency}
    update = self._query(method, path, params)
    return update
